/*
Copyright (C) 2011 The University of Michigan

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Please send inquiries to powertutor@umich.edu
 */

package vn.cybersoft.obs.andriod.batterystats2.components;

import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;
import android.telephony.TelephonyManager;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;

import vn.cybersoft.obs.andriod.batterystats2.phone.PhoneConstants;
import vn.cybersoft.obs.andriod.batterystats2.service.IterationData;
import vn.cybersoft.obs.andriod.batterystats2.service.PowerData;
import vn.cybersoft.obs.andriod.batterystats2.service.PowerEstimator;
import vn.cybersoft.obs.andriod.batterystats2.util.Recycler;
import vn.cybersoft.obs.andriod.batterystats2.util.SystemInfo;

public class Threeg extends PowerComponent {
	public static class ThreegData extends PowerData {
		private static Recycler<ThreegData> recycler = new Recycler<ThreegData>();

		public static ThreegData obtain() {
			ThreegData result = recycler.obtain();
			if(result != null) return result;
			return new ThreegData();
		}

		@Override
		public void recycle() {
			recycler.recycle(this);
		}

		public boolean threegOn;
		public long packets;
		public long uplinkBytes;
		public long downlinkBytes;
		public int powerState;
		public String oper;

		private ThreegData() {
		}

		public void init() {
			threegOn = false;
		}

		public void init(long packets, long uplinkBytes, long downlinkBytes,
				int powerState, String oper) {
			threegOn = true;
			this.packets = packets;
			this.uplinkBytes = uplinkBytes;
			this.downlinkBytes = downlinkBytes;
			this.powerState = powerState;
			this.oper = oper;
		}

		public void writeLogDataInfo(OutputStreamWriter out) throws IOException {
			StringBuilder res = new StringBuilder();
			res.append("3G-on ").append(threegOn).append("\n");
			if(threegOn) {
				res.append("3G-uplinkBytes ").append(uplinkBytes)
				.append("\n3G-downlinkBytes ").append(downlinkBytes)
				.append("\n3G-packets ").append(packets)
				.append("\n3G-state ").append(Threeg.POWER_STATE_NAMES[powerState])
				.append("\n3G-oper ").append(oper)
				.append("\n");
			}
			out.write(res.toString());
		}
	}

	public static final int POWER_STATE_IDLE = 0;
	public static final int POWER_STATE_FACH = 1;
	public static final int POWER_STATE_DCH = 2;
	public static final String[] POWER_STATE_NAMES = {"IDLE", "FACH", "DCH"};

	private static final String TAG = "Threeg";

	private PhoneConstants phoneConstants;
	private TelephonyManager telephonyManager;
	private SystemInfo sysInfo;

	private String oper;
	private int dchFachDelay;
	private int fachIdleDelay;
	private int uplinkQueueSize;
	private int downlinkQueueSize;

	private int[] lastUids;
	private ThreegStateKeeper threegState;
	private SparseArray<ThreegStateKeeper> uidStates;

	private String transPacketsFile;
	private String readPacketsFile;
	private String readBytesFile;
	private String transBytesFile;
	private File uidStatsFolder;

	public Threeg(Context context, PhoneConstants phoneConstants) {
		this.phoneConstants = phoneConstants;
		telephonyManager = (TelephonyManager)context.getSystemService(
				Context.TELEPHONY_SERVICE);

		String interfaceName = phoneConstants.threegInterface();
		threegState = new ThreegStateKeeper();
		uidStates = new SparseArray<ThreegStateKeeper>();
		transPacketsFile = "/sys/devices/virtual/net/" +
				interfaceName + "/statistics/tx_packets";
		readPacketsFile = "/sys/devices/virtual/net/" +
				interfaceName + "/statistics/rx_packets";
		readBytesFile = "/sys/devices/virtual/net/" +
				interfaceName + "/statistics/rx_bytes";
		transBytesFile = "/sys/devices/virtual/net/" +
				interfaceName + "/statistics/tx_bytes";
		uidStatsFolder = new File("/proc/uid_stat");
		sysInfo = SystemInfo.getInstance();
	}

	@Override
	public IterationData calculateIteration(long iteration) {
		IterationData result = IterationData.obtain();

		int netType = telephonyManager.getNetworkType();

		if((netType != TelephonyManager.NETWORK_TYPE_UMTS &&
				netType != 8/* TelephonyManager.NETWORK_TYPE_HSDPA */)) {
			// TODO: Actually get models for the different network types.
			netType = TelephonyManager.NETWORK_TYPE_UMTS;
		}

		if(telephonyManager.getDataState() != TelephonyManager.DATA_CONNECTED ||
				(netType != TelephonyManager.NETWORK_TYPE_UMTS &&
				netType != 8/* TelephonyManager.NETWORK_TYPE_HSDPA */)) {
			/* We need to allow the real iterface state keeper to reset it's state
			 * so that the next update it knows it's coming back from an off state.
			 * We also need to clear all the uid information.
			 */
			oper = null;
			threegState.interfaceOff();
			uidStates.clear();

			ThreegData data = ThreegData.obtain();
			data.init();
			result.setPowerData(data);
			return result;
		}

		if(oper == null) {
			oper = telephonyManager.getNetworkOperatorName();
			dchFachDelay = phoneConstants.threegDchFachDelay(oper);
			fachIdleDelay = phoneConstants.threegFachIdleDelay(oper);
			uplinkQueueSize = phoneConstants.threegUplinkQueue(oper);
			downlinkQueueSize = phoneConstants.threegDownlinkQueue(oper);
		}

		long transmitPackets = readLongFromFile(transPacketsFile);
		long receivePackets = readLongFromFile(readPacketsFile);
		long transmitBytes = readLongFromFile(transBytesFile);
		long receiveBytes = readLongFromFile(readBytesFile);
		if(transmitBytes == -1 || receiveBytes == -1) {
			/* Couldn't read interface data files. */
			Log.w(TAG, "Failed to read packet and byte counts from wifi interface");
			return result;
		}

		if(threegState.isInitialized()) {
			threegState.updateState(transmitPackets, receivePackets,
					transmitBytes, receiveBytes,
					dchFachDelay, fachIdleDelay,
					uplinkQueueSize, downlinkQueueSize);
			ThreegData data = ThreegData.obtain();
			data.init(threegState.getPackets(), threegState.getUplinkBytes(),
					threegState.getDownlinkBytes(), threegState.getPowerState(),
					oper);
			result.setPowerData(data);
		} else {
			threegState.updateState(transmitPackets, receivePackets,
					transmitBytes, receiveBytes,
					dchFachDelay, fachIdleDelay,
					uplinkQueueSize, downlinkQueueSize);
		}

		lastUids = sysInfo.getUids(lastUids);
		if(lastUids != null) for(int uid : lastUids) {
			if(uid == -1) {
				continue;
			}
			try {
				ThreegStateKeeper uidState = uidStates.get(uid);
				if(uidState == null) {
					uidState = new ThreegStateKeeper();
					uidStates.put(uid, uidState);
				}

				if(!uidState.isStale()) {
					/* We use a huerstic here so that we don't poll for uids that haven't
					 * had much activity recently.
					 */
					continue;
				}

				/* These read operations are the expensive part of polling. */
				receiveBytes = readLongFromFile("/proc/uid_stat/" + uid + "/tcp_rcv");
				transmitBytes = readLongFromFile("/proc/uid_stat/" + uid + "/tcp_snd");

				if(receiveBytes == -1 || transmitBytes == -1) {
					Log.w(TAG, "Failed to read uid read/write byte counts");
				} else if(uidState.isInitialized()) {
					uidState.updateState(-1, -1, transmitBytes, receiveBytes,
							dchFachDelay, fachIdleDelay,
							uplinkQueueSize, downlinkQueueSize);

					if(uidState.getUplinkBytes() + uidState.getDownlinkBytes() != 0 ||
							uidState.getPowerState() != POWER_STATE_IDLE) {
						ThreegData uidData = ThreegData.obtain();
						uidData.init(uidState.getPackets(),
								uidState.getUplinkBytes(), uidState.getDownlinkBytes(),
								uidState.getPowerState(), oper);
						result.addUidPowerData(uid, uidData);
					}
				} else {
					uidState.updateState(-1, -1, transmitBytes, receiveBytes,
							dchFachDelay, fachIdleDelay,
							uplinkQueueSize, downlinkQueueSize);
				}
			} catch(NumberFormatException e) {
				Log.w(TAG, "Non-uid files in /proc/uid_stat");
			}
		}

		return result;
	}

	private static class ThreegStateKeeper {
		private long lastTransmitPackets;
		private long lastReceivePackets;
		private long lastTransmitBytes;
		private long lastReceiveBytes;
		private long lastTime;

		private long deltaPackets;
		private long deltaUplinkBytes;
		private long deltaDownlinkBytes;

		private int powerState;
		private int stateTime;

		private long inactiveTime;

		public ThreegStateKeeper() {
			lastTransmitBytes = lastReceiveBytes = lastTime = -1;
			deltaUplinkBytes = deltaDownlinkBytes = -1;
			powerState = POWER_STATE_IDLE;
			stateTime = 0;
			inactiveTime = 0;
		}

		public void interfaceOff() {
			lastTime = SystemClock.elapsedRealtime();
			powerState = POWER_STATE_IDLE;
		}

		public boolean isInitialized() {
			return lastTime != -1;
		}

		public void updateState(long transmitPackets, long receivePackets,
				long transmitBytes, long receiveBytes,
				int dchFachDelay, int fachIdleDelay,
				int uplinkQueueSize, int downlinkQueueSize) {
			long curTime = SystemClock.elapsedRealtime();
			if(lastTime != -1 && curTime > lastTime) {
				double deltaTime = curTime - lastTime;
				deltaPackets = transmitPackets + receivePackets -
						lastTransmitPackets - lastReceivePackets;
				deltaUplinkBytes = transmitBytes - lastTransmitBytes;
				deltaDownlinkBytes = receiveBytes - lastReceiveBytes;
				boolean inactive = deltaUplinkBytes == 0 && deltaDownlinkBytes == 0;
				inactiveTime = inactive ? inactiveTime + curTime - lastTime : 0;

				// TODO: make this always work.
				int timeMult = 1;
				if(1000 % PowerEstimator.ITERATION_INTERVAL != 0) {
					Log.w(TAG,
							"Cannot handle iteration intervals that are a factor of 1 second");
				} else {
					timeMult = 1000 / PowerEstimator.ITERATION_INTERVAL;
				}

				switch(powerState) {
				case POWER_STATE_IDLE:
					if(!inactive) {
						powerState = POWER_STATE_FACH;
					}
					break;
				case POWER_STATE_FACH:
					if(inactive) {
						stateTime++;
						if(stateTime >= fachIdleDelay * timeMult) {
							stateTime = 0;
							powerState = POWER_STATE_IDLE;
						}
					} else {
						stateTime = 0;
						if(deltaUplinkBytes > 0 ||
								deltaDownlinkBytes > 0) {
							powerState = POWER_STATE_DCH;
						}
					}
					break;
				default: // case POWER_STATE_DCH:
					if(inactive) {
						stateTime++;
						if(stateTime >= dchFachDelay * timeMult) {
							stateTime = 0;
							powerState = POWER_STATE_FACH;
						}
					} else {
						stateTime = 0;
					}
				}
			}
			lastTime = curTime;
			lastTransmitPackets = transmitPackets;
			lastReceivePackets = receivePackets;
			lastTransmitBytes = transmitBytes;
			lastReceiveBytes = receiveBytes;
		}

		public int getPowerState() {
			return powerState;
		}

		public long getPackets() {
			return deltaPackets;
		}

		public long getUplinkBytes() {
			return deltaUplinkBytes;
		}

		public long getDownlinkBytes() {
			return deltaDownlinkBytes;
		}

		/* The idea here is that we don't want to have to read uid information
		 * every single iteration for each uid as it just takes too long.  So here
		 * we are designing a hueristic that helps us avoid polling for too many
		 * uids.
		 */
		public boolean isStale() {
			if(powerState != POWER_STATE_IDLE) return true;
			long curTime = SystemClock.elapsedRealtime();
			return curTime - lastTime > (long)Math.min(10000, inactiveTime);
		}
	}

	private final static byte[] buf = new byte[16];

	private long readLongFromFile(String filePath) {
		return sysInfo.readLongFromFile(filePath);
	}

	@Override
	public boolean hasUidInformation() {
		return uidStatsFolder.exists();
	}

	@Override
	public String getComponentName() {
		return "3G";
	}
}
